﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Model.Runtime;
using Kaleida.ServiceMonitor.Framework;

namespace Kaleida.ServiceMonitor.Model.ColourSpecifications
{
    internal class RangeStateColourSpecification : IStateColourSpecification
    {
        private readonly IMetric metric = FailureCountMetric.Instance;

        private readonly Color colour;
        private readonly decimal? minimum;
        private readonly decimal? maximum;

        public static RangeStateColourSpecification WithBounds(Color colour, [NotNull] IMetric metric, decimal minimum, decimal maximum)
        {
            return new RangeStateColourSpecification(colour, metric, minimum, maximum);
        }

        public static RangeStateColourSpecification SingleValue(Color colour, [NotNull] IMetric metric, decimal value)
        {
			if (value < 0) throw new ArgumentOutOfRangeException("value", value, "Must be zero or more");
            return new RangeStateColourSpecification(colour, metric, value, value);
        }

        public static RangeStateColourSpecification WithLowerBound(Color colour, [NotNull] IMetric metric, decimal minimum)
        {
            return new RangeStateColourSpecification(colour, metric, minimum, null);
        }

        public static RangeStateColourSpecification WithUpperBound(Color colour, [NotNull] IMetric metric, decimal maximum)
        {
            return new RangeStateColourSpecification(colour, metric, null, maximum);
        }

        public static RangeStateColourSpecification Parse(string text)
        {
            return Parse(text, FailureCountMetric.Instance);
        }

        public static RangeStateColourSpecification Parse(string text, [NotNull] IMetric defaultMetric)
        {
            if (defaultMetric == null) throw new ArgumentNullException("defaultMetric");
            return Parser.ParseInput(text, defaultMetric);
        }

        private RangeStateColourSpecification(Color colour, [NotNull] IMetric metric, decimal? minimum, decimal? maximum)
        {
        	if (metric == null) throw new ArgumentNullException("metric");
        	if (minimum < 0) throw new ArgumentOutOfRangeException("minimum", minimum, "Must be zero or more");
            if (maximum < 0) throw new ArgumentOutOfRangeException("maximum", maximum, "Must be zero or more");
			if (minimum > maximum) throw new ArgumentException(string.Format("minimum cannot be more than maximum. minimum={0}, maximum={1}", minimum, maximum));

            if (minimum != null)
        	    ThrowIfInvalid(metric, minimum.Value, "minimum");

            if(maximum != null)
			    ThrowIfInvalid(metric, maximum.Value, "maximum");

            this.colour = colour;
        	this.metric = metric;
        	this.minimum = minimum;
            this.maximum = maximum;
        }

		private static void ThrowIfInvalid(IMetric metric, decimal value, string name)
		{
			string message;
			if (!metric.IsValidValue(value, out message))
                throw new ArgumentOutOfRangeException(name, value, string.Format("Metric '{0}': {1}", metric.NamePlural, message));
		}

        public Color Colour
        {
            get { return colour; }
        }

        public IMetric Metric
        {
            get { return metric; }
        }

        public bool IsMatch(IMonitorState state)
        {
            var value = metric.GetValueFunction(state);

            if (value == null)
                return false;

            var greaterThanMinimum = minimum == null || value >= minimum.Value;
            var lessThanMaximum = maximum == null || value <= maximum.Value;
            return greaterThanMinimum && lessThanMaximum;
        }

        public string Description
        {
            get
            {
                var description = new StringBuilder();

                if (minimum == null && maximum == null)
                    description.AppendFormat("Any number of {0}: ", metric.NamePlural);
                else if (minimum == maximum)
                    description.AppendFormat("{0} {1}: ", minimum, minimum == 1 ? metric.NameSingular : metric.NamePlural);
                else if (maximum == null)
                    description.AppendFormat("{0} or more {1}: ", minimum, metric.NamePlural);
                else if (minimum == null)
                    description.AppendFormat("{0} or fewer {1}: ", maximum, metric.NamePlural);
                else
					description.AppendFormat("{0} to {1} {2}: ", minimum, maximum, metric.NamePlural);

                description.Append(colour.Name);
                return description.ToString();
            }
        }

        private class Parser
        {
            private readonly IMetric defaultMetric; // not yet supported
            private readonly List<Func<string, RangeStateColourSpecification>> patternMatchers;

            private static Regex NewRegex(string pattern)
            {
                return new Regex(pattern, RegexOptions.Compiled);
            }

            private class RegexCandidateList : IEnumerable<Regex>
            {
                private readonly List<Regex> list = new List<Regex>();

                public void Add(Regex regex)
                {
                    list.Add(regex);
                }

                [CanBeNull]
                public Match GetFirstMatch(string value)
                {
                    return (from c in list
                            let m = c.Match(value)
                            where m.Success
                            select m).FirstOrDefault();
                }

                public IEnumerator<Regex> GetEnumerator()
                {
                    return list.GetEnumerator();
                }

                IEnumerator IEnumerable.GetEnumerator()
                {
                    return GetEnumerator();
                }
            }

            private const string NumberPattern = @"(?<number>-?[0-9.]+%?)";
            private const string NonNegativeNumberPattern = @"(?<number>[0-9.]+%?)";
            private const string ColourNamePattern = @"(?<colour>[A-Za-z]+)";
            private const string MetricPattern = @"(?<metric>[-A-Za-z0-9]+ )?";

            private static readonly RegexCandidateList singleValueCandidates = new RegexCandidateList
                                                                                   {
                                                                                       NewRegex(@"^\s*" + NumberPattern + @"\s+" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$")
                                                                                   };

            private static readonly RegexCandidateList boundedCandidates = new RegexCandidateList
                                                                               {
                                                                                   NewRegex(@"^\s*" + NumberPattern + @"\s*->\s*" + NumberPattern + @"\s+" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$"),
                                                                                   NewRegex(@"^\s*" + NumberPattern + @"\s*→\s*" + NumberPattern + @"\s+" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$"),
                                                                                   NewRegex(@"^\s*" + NonNegativeNumberPattern + @"\s*-\s*" + NonNegativeNumberPattern + @"\s+" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$")
                                                                               };

            private static readonly RegexCandidateList upperBoundedCandidates = new RegexCandidateList
                                                                                    {
                                                                                        NewRegex(@"^\s*->\s*" + NumberPattern+ @"\s+" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$"),
                                                                                        NewRegex(@"^\s*→\s*" + NumberPattern + @"\s+" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$")
                                                                                    };

            private static readonly RegexCandidateList lowerBoundedCandidates = new RegexCandidateList
                                                                                    {
                                                                                        NewRegex(@"^\s*" + NumberPattern + @"\s*->\s*" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$"),
                                                                                        NewRegex(@"^\s*" + NumberPattern + @"\s*→\s*" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$"),
                                                                                        NewRegex(@"^\s*" + NumberPattern + @"\s*\+\s*" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$"),
                                                                                        NewRegex(@"^\s*" + NumberPattern + @"\s*-\s*" + MetricPattern + @"\s*" + ColourNamePattern + @"\s*$")
                                                                                    };

            private Parser(IMetric defaultMetric)
            {
                this.defaultMetric = defaultMetric;
                patternMatchers = new List<Func<string, RangeStateColourSpecification>>
            	                  	{
            	                  		v => TryMatchSingleValue(v),
            	                  		v => TryMatchBoundedContinuum(v),
            	                  		v => TryMatchLowerBoundedContinuum(v),
            	                  		v => TryMatchUpperBoundedContinuum(v),
            	                  	};
            }

            public static RangeStateColourSpecification ParseInput(string text, [NotNull] IMetric defaultMetric)
            {
                if (defaultMetric == null) throw new ArgumentNullException("defaultMetric");

                var parser = new Parser(defaultMetric);
                return parser.ParseInput(text);
            }

            private RangeStateColourSpecification ParseInput(string text)
            {
                var results = patternMatchers.Select(m => m(text));

                var specification = results.FirstOrDefault(s => s != null);

                if (specification == null)
                    throw new FormatException(string.Format("'{0}' does not contain a recognisable colour range specification. Examples of valid specifications are: '0 failures Green', '10%-50% success-rate blue', '2-9 failures Orange' and '100+ failures Crimson'", text));

                return specification;
            }

            [CanBeNull]
            private RangeStateColourSpecification TryMatchBoundedContinuum(string value)
            {
                var match = boundedCandidates.GetFirstMatch(value);
                if (match == null) return null;

                if(match.Groups["number"].Captures.Count != 2)
                    throw new InvalidOperationException("Expected two regex captures of the 'number' group");

                string minString = match.Groups["number"].Captures[0].Value;
                string maxString = match.Groups["number"].Captures[1].Value;
                string metricName = match.Groups["metric"].Value;
				string colourName = match.Groups["colour"].Value;
                var metric = GetMetric(metricName);
                return WithBounds(ConvertToColour(colourName), metric, ConvertToDecimal(minString, "minimum"), ConvertToDecimal(maxString, "maximum"));
            }

        	[CanBeNull]
            private RangeStateColourSpecification TryMatchSingleValue(string value)
            {
                var match = singleValueCandidates.GetFirstMatch(value);
                if (match == null) return null;

                if (match.Groups["number"].Captures.Count != 1)
                    throw new InvalidOperationException("Expected one regex captures of the 'number' group");

                string numberString = match.Groups["number"].Value;
                string metricName = match.Groups["metric"].Value;
                string colourName = match.Groups["colour"].Value;

        		var number = ConvertToDecimal(numberString, "value");
        	    var metric = GetMetric(metricName);

                return SingleValue(ConvertToColour(colourName), metric, number);
            }

        	[CanBeNull]
            private RangeStateColourSpecification TryMatchUpperBoundedContinuum(string value)
            {
                var match = upperBoundedCandidates.GetFirstMatch(value);
                if (match == null) return null;

                if (match.Groups["number"].Captures.Count != 1)
                    throw new InvalidOperationException("Expected one regex captures of the 'number' group");

                string maxString = match.Groups["number"].Value;
                string metricName = match.Groups["metric"].Value;
                string colourName = match.Groups["colour"].Value;

                var metric = GetMetric(metricName);

                return WithUpperBound(ConvertToColour(colourName), metric, ConvertToDecimal(maxString, "maximum"));

            }

        	[CanBeNull]
            private RangeStateColourSpecification TryMatchLowerBoundedContinuum(string value)
            {
                var match = lowerBoundedCandidates.GetFirstMatch(value);
                if (match == null) return null;

                if (match.Groups["number"].Captures.Count != 1)
                    throw new InvalidOperationException("Expected one regex captures of the 'number' group");

                string minString = match.Groups["number"].Value;
                string metricName = match.Groups["metric"].Value;
                string colourName = match.Groups["colour"].Value;

                var metric = GetMetric(metricName);

                return WithLowerBound(ConvertToColour(colourName), metric, ConvertToDecimal(minString, "minimum"));
            }

        	private static Color ConvertToColour(string colourName)
        	{
				KnownColor colour;
				if (Enum.TryParse(colourName, true, out colour))
					return Color.FromKnownColor(colour);
				throw new FormatException(string.Format("'{0}' is not a known colour. Some examples of colours are 'Red', 'Blue', 'Orange' and 'Green'", colourName));
        	}

        	private static decimal ConvertToDecimal(string numberString, string name)
        	{
                Percentage percentage;
                if (Percentage.TryParse(numberString, out percentage))
                    return percentage.Value;

        		decimal number;
				if (decimal.TryParse(numberString, out number))
        			return number;
                
        		throw new FormatException(string.Format("Cannot convert {0} value '{1}' into a number", name, numberString));
        	}

            private IMetric GetMetric(string metricName)
            {
                var trimmedLowerCasedName = metricName.Trim().ToLowerInvariant();

                if (trimmedLowerCasedName == "") // Default not yet supported
                    throw new InvalidOperationException("You must specify a metric. E.g. '2-9 failures Orange'");

                var knownMetrics = new[]
                                       {
                                           FailureCountMetric.Instance,
                                           SuccessRateMetric.Instance
                                       };

                var metric = knownMetrics.FirstOrDefault(i => i.NameSingular.ToLowerInvariant() == trimmedLowerCasedName || i.NamePlural.ToLowerInvariant() == trimmedLowerCasedName);
                if (metric == null)
                    throw new InvalidOperationException(string.Format("Unknown metric '{0}'. Must be {1}", trimmedLowerCasedName, knownMetrics.Select(i => "'" + i.NamePlural + "'").JoinWith(", ", " or ")));

                return metric;
            }
        }
    }
}